import json
import os.path
import base64
import traceback
import re

from urllib.parse import urlencode
from functools import lru_cache
from datetime import datetime

from tornado.web import (
    HTTPError,
    RequestHandler,
)
from tornado.gen import coroutine
from tornado.httpclient import (
    AsyncHTTPClient,
    HTTPRequest,
)
from tornado.options import options

from elasticsearch import Elasticsearch
from elasticsearch_dsl import Search

from ESMapping import Images


class JSONHTTPError(HTTPError):
    def __init__(self, status_code, log_message=None, *args, **kwargs):
        self.response = kwargs.get('response', {})
        super().__init__(status_code, log_message, *args, **kwargs)


class JSONHandler(RequestHandler):
    def set_default_headers(self):
        self.set_header("Content-Type", "application/json; charset=UTF-8")

    def prepare(self):
        if self.request.method not in ('GET', 'HEAD'):
            try:
                body = self.request.body.decode('utf8')
                if body:
                    json_args = json.loads(self.request.body.decode('utf8'))
                    self.json_args = dict()
                    for key, value in json_args.items():
                        self.json_args[key] = [value]
                else:
                    self.json_args = None
            except Exception as e:
                raise JSONHTTPError(415) from e

    def write_error(self, status_code, **kwargs):
        if self.settings.get("serve_traceback") and "exc_info" in kwargs:
            # in debug mode, try to send a traceback
            self.set_header('Content-Type', 'text/plain')
            for line in traceback.format_exception(*kwargs["exc_info"]):
                self.write(line)
            exception = kwargs['exc_info'][1]
            if isinstance(exception, JSONHTTPError):
                self.write(exception.response)
            self.finish()
        else:
            response = kwargs.get('response', None)
            if "exc_info" in kwargs:
                exception = kwargs['exc_info'][1]
                if isinstance(exception, JSONHTTPError) and exception.response:
                    response = exception.response

            if response is None:
                response = [
                    {
                        'error': self._reason
                    }
                ]
            if not (isinstance(response, str) or isinstance(response, bytes)):
                response = json.dumps(response)
            self.set_status(status_code)
            self.set_header("Content-Type", "application/json; charset=UTF-8")
            self.finish(response)


class UploadHandler(JSONHandler):
    def prepare(self):
        pass

    @coroutine
    def put(self, filename):
        filename = filename.replace('/', '')
        file = self.request.body
        self.save_file(filename, file)
        cv_result = yield self.ms_cv_analyze(file)
        image = self.save_es(filename, cv_result)
        self.finish(image.to_dict())

    @staticmethod
    @coroutine
    def ms_cv_analyze(file):
        cv_response = yield AsyncHTTPClient().fetch(HTTPRequest(
            'https://api.projectoxford.ai/vision/v1.0/analyze?visualFeatures=Categories,Tags,Description',
            headers={
                "Content-Type": "application/octet-stream",
                "Ocp-Apim-Subscription-Key": options.ms_cv_key,
            },
            method="POST",
            body=file,
        ))
        cv_result = json.loads(cv_response.body.decode('utf8'))
        return cv_result

    @staticmethod
    def save_file(filename, file):
        filename = os.path.join(options.static_dir, filename)
        with open(filename, 'wb') as f:
            f.write(file)
        f.close()

    @staticmethod
    def save_es(filename, cv_result):
        categories = [category['name'].strip('_') for category in cv_result.get('categories', [])]
        tags = cv_result.get('description', {}).get('tags', [])
        descriptions = [caption['text'] for caption in cv_result.get('description', {}).get('captions', [])]

        _filename = filename
        _filename = _filename.replace('/', '')
        _filename = _filename.replace('_', '.')
        _filename = _filename.replace('-', '.')
        _filename = _filename.replace('|', '.')
        names = _filename.split('.')

        image = Images(
            categories=categories,
            tags=tags,
            descriptions=descriptions,
            names=names,
            filename=filename,
        )
        image.meta.id = base64.b64encode(filename.encode('utf8')).decode('ascii')
        image.save()
        return image


class QueryHandler(JSONHandler):
    ENGLISH_RE = re.compile(r"^[0-9a-zA-Z ]+$")
    REMOVE_TAG_RE = re.compile(r"<[^>]+>")
    ITEMS_PER_PAGE = 10

    @coroutine
    def get(self):
        key = self.get_argument('key', '')
        page = int(self.get_argument('page', 0))
        if not self.ENGLISH_RE.findall(key):
            translate = yield self.translate(key)
            key = translate
        s = Search().using(Elasticsearch(hosts=[options.es_host]))
        s = s.query('multi_match', query=key, fields=['descriptions', 'categories'])
        s = s[page*self.ITEMS_PER_PAGE: (page+1)*self.ITEMS_PER_PAGE]
        response = s.execute()
        hits = response.to_dict()['hits']
        result = {
            'total': hits['total'],
            'hits': [hit['_source']['filename'] for hit in hits['hits']],
        }
        l = [",".join(hit['_source']['descriptions']) for hit in hits['hits']]
        self.finish(result)

    azure_token = None
    azure_token_ttl = 7000
    azure_token_datetime = None

    @classmethod
    @coroutine
    def fetch_azure_token(cls, client_id, client_secret, scope):
        now = datetime.now()
        if cls.azure_token_datetime is not None \
            and cls.azure_token is not None \
                and (now - cls.azure_token_datetime).total_seconds() < cls.azure_token_ttl:
            return cls.azure_token
        auth = yield AsyncHTTPClient().fetch(HTTPRequest(
            'https://datamarket.accesscontrol.windows.net/v2/OAuth2-13',
            headers={'Content-Type': 'application/x-www-form-urlencoded'},
            method='POST',
            body=urlencode({
                'grant_type': 'client_credentials',
                'client_id': client_id,
                'client_secret': client_secret,
                'scope': scope,
            }),
        ))
        response = json.loads(auth.body.decode('utf8'))
        token = response.get('access_token', None)
        if token is None:
            raise JSONHTTPError(500)
        cls.azure_token = token
        cls.azure_token_datetime = datetime.now()
        return token

    @classmethod
    @lru_cache()
    @coroutine
    def translate(cls, key):
        token = yield cls.fetch_azure_token(
            options.azure_client_id,
            options.azure_client_secret,
            'http://api.microsofttranslator.com',
        )
        token = 'Bearer ' + token
        translate = yield AsyncHTTPClient().fetch(HTTPRequest(
            'http://api.microsofttranslator.com/v2/Http.svc/Translate?' + urlencode({
                'text': key, 'from': 'zh', 'to': 'en'
            }),
            headers={'Authorization': token},
            method='GET',
        ))
        result = cls.REMOVE_TAG_RE.sub('', translate.body.decode('utf8'))
        return result
